package com.swak.fx.support.ui;

import java.util.List;
import java.util.function.Function;

import com.sun.javafx.event.EventHandlerManager;
import com.swak.fx.support.transition.CachedTransition;

import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventDispatchChain;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogEvent;
import javafx.scene.control.DialogPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;

/**
 * UIDialog.
 * 
 * @author 618lf
 */
public class UIDialog extends javafx.scene.control.Dialog<ButtonType> implements UILifecycle {

	private final String STYLE_CLASS = "ui-dialog";
	private final String OVERLAY_STYLE_CLASS = "ui-dialog-overlay";
	private final String STYLESHEET = UI.load("css/UIDialog.css");
	private final DialogPane dialogInner;
	private final StackPane container;
	private final Window owerWindow;

	private InvalidationListener widthListener = observable -> updateWidth();;
	private InvalidationListener heightListener = observable -> updateHeight();
	private InvalidationListener xListener = observable -> updateX();
	private InvalidationListener yListener = observable -> updateY();

	private boolean animateClosing = true;

	public UIDialog(Window window) {
		this.owerWindow = window;
		this.dialogInner = new UIDialogPane();
		this.container = new StackPane();
		this.init();
	}

	@Override
	public List<String> getStylesheets() {
		return container.getStylesheets();
	}

	@Override
	public void initializeParts() {
		this.container.getStyleClass().add(STYLE_CLASS);
		this.dialogInner.getStyleClass().add(OVERLAY_STYLE_CLASS);
		final Node materialNode = UI.Shadows.LEVEL2.wrapControl(container);
		materialNode.setPickOnBounds(false);
		materialNode.addEventHandler(MouseEvent.MOUSE_CLICKED, Event::consume);
		this.dialogInner.setContent(materialNode);
		this.setDialogPane(this.dialogInner);
		this.dialogInner.getScene().setFill(Color.TRANSPARENT);
		this.initModality(Modality.APPLICATION_MODAL);
		this.initStyle(StageStyle.TRANSPARENT);
		this.initOwner(this.owerWindow);
	}

	@Override
	public void setupEventHandlers() {
		this.dialogInner.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
			if (this.isOverlayClose()) {
				hide();
			}
		});

		this.dialogInner.getScene().getWindow().addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
			if (keyEvent.getCode() == KeyCode.ESCAPE) {
				if (!isHideOnEscape()) {
					keyEvent.consume();
				}
			}
		});

		// handle animation / owner window layout changes
		eventHandlerManager.addEventHandler(DialogEvent.DIALOG_SHOWING, event -> {
			addLayoutListeners();
			DialogAnimation animation = getCurrentAnimation();
			animation.initAnimation(this.container.getParent(), this.dialogInner);
		});
		eventHandlerManager.addEventHandler(DialogEvent.DIALOG_SHOWN, event -> {
			if (getOwner() != null) {
				updateLayout();
			}

			animateClosing = true;
			Animation animation = getCurrentAnimation().createShowingAnimation(this.container.getParent(),
					this.dialogInner);
			if (animation != null) {
				animation.play();
			}
		});
		eventHandlerManager.addEventHandler(DialogEvent.DIALOG_CLOSE_REQUEST, event -> {
			if (animateClosing) {
				event.consume();
				hideWithAnimation();
			}
		});
		eventHandlerManager.addEventHandler(DialogEvent.DIALOG_HIDDEN, event -> removeLayoutListeners());
	}

	private Animation transition = null;

	private void hideWithAnimation() {
		if (transition == null || transition.getStatus().equals(Animation.Status.STOPPED)) {
			Animation animation = getCurrentAnimation().createHidingAnimation(this.container.getParent(),
					this.dialogInner);
			if (animation != null) {
				transition = animation;
				animation.setOnFinished(finish -> {
					animateClosing = false;
					hide();
					transition = null;
				});
				animation.play();
			} else {
				animateClosing = false;
				transition = null;
				Platform.runLater(this::hide);
			}
		}
	}

	// this method ensure not null value for current animation
	private DialogAnimation getCurrentAnimation() {
		DialogAnimation usedAnimation = animationProperty().get();
		usedAnimation = usedAnimation == null ? CENTER_ANIMATION : usedAnimation;
		return usedAnimation;
	}

	@Override
	public void setupValueChangedListeners() {
		contentNodeProperty().addListener((ob, o, n) -> {
			this.container.getChildren().remove(o);
			this.container.getChildren().add(n);
		});
	}

	private void removeLayoutListeners() {
		Window stage = getOwner();
		if (stage != null) {
			stage.getScene().widthProperty().removeListener(widthListener);
			stage.getScene().heightProperty().removeListener(heightListener);
			stage.xProperty().removeListener(xListener);
			stage.yProperty().removeListener(yListener);
		}
	}

	private void addLayoutListeners() {
		Window stage = getOwner();
		if (stage != null) {
			stage.getScene().widthProperty().addListener(widthListener);
			stage.getScene().heightProperty().addListener(heightListener);
			stage.xProperty().addListener(xListener);
			stage.yProperty().addListener(yListener);
		}
	}

	private void updateLayout() {
		updateX();
		updateY();
		updateWidth();
		updateHeight();
	}

	private void updateHeight() {
		Window stage = getOwner();
		setHeight(stage.getScene().getHeight());
	}

	private void updateWidth() {
		Window stage = getOwner();
		setWidth(stage.getScene().getWidth());
	}

	private void updateY() {
		Window stage = getOwner();
		setY(stage.getY() + stage.getScene().getY());
	}

	private void updateX() {
		Window stage = getOwner();
		setX(stage.getX() + stage.getScene().getX());
	}

	class UIDialogPane extends DialogPane {
		private boolean performingLayout = false;
		{
			getButtonTypes().add(ButtonType.CLOSE);
			Node closeButton = this.lookupButton(ButtonType.CLOSE);
			closeButton.managedProperty().bind(closeButton.visibleProperty());
			closeButton.setVisible(false);
		}

		@Override
		protected double computePrefHeight(double width) {
			Window owner = getOwner();
			if (owner != null) {
				return owner.getHeight();
			} else {
				return super.computePrefHeight(width);
			}
		}

		@Override
		protected double computePrefWidth(double height) {
			Window owner = getOwner();
			if (owner != null) {
				return owner.getWidth();
			} else {
				return super.computePrefWidth(height);
			}
		}

		@Override
		public void requestLayout() {
			if (performingLayout) {
				return;
			}
			super.requestLayout();
		}

		@Override
		protected void layoutChildren() {
			performingLayout = true;
			List<Node> managed = getManagedChildren();
			final double width = getWidth();
			double height = getHeight();
			double top = getInsets().getTop();
			double right = getInsets().getRight();
			double left = getInsets().getLeft();
			double bottom = getInsets().getBottom();
			double contentWidth = width - left - right;
			double contentHeight = height - top - bottom;
			for (Node child : managed) {
				layoutInArea(child, left, top, contentWidth, contentHeight, 0, Insets.EMPTY, HPos.CENTER, VPos.CENTER);
			}
			performingLayout = false;
		}

		@Override
		public String getUserAgentStylesheet() {
			return STYLESHEET;
		}

		@Override
		protected Node createButtonBar() {
			return null;
		}
	}

	private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);

	@Override
	public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
		return super.buildEventDispatchChain(tail).prepend(eventHandlerManager);
	}

	private BooleanProperty overlayClose = new SimpleBooleanProperty(true);

	public boolean isOverlayClose() {
		return overlayClose.get();
	}

	public BooleanProperty overlayCloseProperty() {
		return overlayClose;
	}

	public void setOverlayClose(boolean overlayClose) {
		this.overlayClose.set(overlayClose);
	}

	private BooleanProperty hideOnEscape = new SimpleBooleanProperty(this, "hideOnEscape", true);

	public final void setHideOnEscape(boolean value) {
		hideOnEscape.set(value);
	}

	public final boolean isHideOnEscape() {
		return hideOnEscape.get();
	}

	public final BooleanProperty hideOnEscapeProperty() {
		return hideOnEscape;
	}

	private final ObjectProperty<Node> contentNode = new SimpleObjectProperty<Node>(this, "contentNode") { //$NON-NLS-1$
		@Override
		public void setValue(Node node) {
			if (node == null) {
				throw new IllegalArgumentException("content node can not be null"); //$NON-NLS-1$
			}
		};
	};

	public final ObjectProperty<Node> contentNodeProperty() {
		return contentNode;
	}

	public final Node getContentNode() {
		return contentNodeProperty().get();
	}

	public final void setContentNode(Node content) {
		contentNodeProperty().set(content);
	}

	private ObjectProperty<DialogAnimation> animation = new SimpleObjectProperty<>(CENTER_ANIMATION);

	public ObjectProperty<DialogAnimation> animationProperty() {
		return animation;
	}

	// ================================================================================
	// Animations
	// ================================================================================

	public static interface DialogAnimation {
		Function<Transition, Transition> inverseAnimation = transition -> {
			transition.jumpTo(transition.getCycleDuration());
			transition.setRate(-1);
			return transition;
		};

		void initAnimation(Node contentContainer, Node overlay);

		Animation createShowingAnimation(Node contentContainer, Node overlay);

		Animation createHidingAnimation(Node contentContainer, Node overlay);
	}

	public static DialogAnimation LEFT_ANIMATION = new DialogAnimation() {

		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer
					.setTranslateX(-(contentContainer.getLayoutX() + contentContainer.getLayoutBounds().getMaxX()));
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new HorizontalTransition(true, contentContainer, overlay);
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return inverseAnimation.apply(new HorizontalTransition(true, contentContainer, overlay));
		}
	};

	public static DialogAnimation RIGHT_ANIMATION = new DialogAnimation() {
		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer
					.setTranslateX(contentContainer.getLayoutX() + contentContainer.getLayoutBounds().getMaxX());
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new HorizontalTransition(false, contentContainer, overlay);
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return inverseAnimation.apply(new HorizontalTransition(false, contentContainer, overlay));
		}
	};

	public static DialogAnimation TOP_ANIMATION = new DialogAnimation() {
		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer
					.setTranslateY(-(contentContainer.getLayoutY() + contentContainer.getLayoutBounds().getMaxY()));
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new VerticalTransition(true, contentContainer, overlay);
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return inverseAnimation.apply(new VerticalTransition(true, contentContainer, overlay));
		}
	};

	public static DialogAnimation BOTTOM_ANIMATION = new DialogAnimation() {
		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer
					.setTranslateY(contentContainer.getLayoutY() + contentContainer.getLayoutBounds().getMaxY());
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new VerticalTransition(false, contentContainer, overlay);
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return inverseAnimation.apply(new VerticalTransition(false, contentContainer, overlay));
		}
	};

	public static DialogAnimation CENTER_ANIMATION = new DialogAnimation() {
		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer.setScaleX(0);
			contentContainer.setScaleY(0);
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new CenterTransition(contentContainer, overlay);
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return inverseAnimation.apply(new CenterTransition(contentContainer, overlay));
		}
	};

	static DialogAnimation SMOOTH = new DialogAnimation() {

		@Override
		public void initAnimation(Node contentContainer, Node overlay) {
			overlay.setOpacity(0);
			contentContainer.setScaleX(.80);
			contentContainer.setScaleY(.80);
		}

		@Override
		public Animation createShowingAnimation(Node contentContainer, Node overlay) {
			return new CachedTransition(contentContainer,
					new Timeline(new KeyFrame(Duration.millis(1000),
							new KeyValue(contentContainer.scaleXProperty(), 1, Interpolator.EASE_OUT),
							new KeyValue(contentContainer.scaleYProperty(), 1, Interpolator.EASE_OUT),
							new KeyValue(overlay.opacityProperty(), 1, Interpolator.EASE_BOTH)))) {
				{
					setCycleDuration(Duration.millis(160));
					setDelay(Duration.seconds(0));
				}
			};
		}

		@Override
		public Animation createHidingAnimation(Node contentContainer, Node overlay) {
			return new CachedTransition(contentContainer, new Timeline(new KeyFrame(Duration.millis(1000),
					new KeyValue(overlay.opacityProperty(), 0, Interpolator.EASE_BOTH)))) {
				{
					setCycleDuration(Duration.millis(160));
					setDelay(Duration.seconds(0));
				}
			};
		}
	};

	static class CenterTransition extends CachedTransition {
		public CenterTransition(Node contentContainer, Node overlay) {
			super(contentContainer, new Timeline(
					new KeyFrame(Duration.ZERO, new KeyValue(contentContainer.scaleXProperty(), 0, Interpolator.LINEAR),
							new KeyValue(contentContainer.scaleYProperty(), 0, Interpolator.LINEAR),
							new KeyValue(overlay.opacityProperty(), 0, Interpolator.EASE_BOTH)),
					new KeyFrame(Duration.millis(1000),
							new KeyValue(contentContainer.scaleXProperty(), 1, Interpolator.EASE_OUT),
							new KeyValue(contentContainer.scaleYProperty(), 1, Interpolator.EASE_OUT),
							new KeyValue(overlay.opacityProperty(), 1, Interpolator.EASE_BOTH))));
			// reduce the number to increase the shifting , increase number to reduce
			// shifting
			setCycleDuration(Duration.seconds(0.4));
			setDelay(Duration.seconds(0));
		}
	}

	static class HorizontalTransition extends CachedTransition {
		public HorizontalTransition(boolean leftDirection, Node contentContainer, Node overlay) {
			super(contentContainer, new Timeline(
					new KeyFrame(Duration.ZERO,
							new KeyValue(contentContainer.translateXProperty(),
									(contentContainer.getLayoutX() + contentContainer.getLayoutBounds().getMaxX())
											* (leftDirection ? -1 : 1),
									Interpolator.LINEAR),
							new KeyValue(overlay.opacityProperty(), 0, Interpolator.EASE_BOTH)),
					new KeyFrame(Duration.millis(1000),
							new KeyValue(overlay.opacityProperty(), 1, Interpolator.EASE_BOTH),
							new KeyValue(contentContainer.translateXProperty(), 0, Interpolator.EASE_OUT))));
			// reduce the number to increase the shifting , increase number to reduce
			// shifting
			setCycleDuration(Duration.seconds(0.4));
			setDelay(Duration.seconds(0));
		}
	}

	static class VerticalTransition extends CachedTransition {
		public VerticalTransition(boolean topDirection, Node contentContainer, Node overlay) {
			super(contentContainer, new Timeline(
					new KeyFrame(Duration.ZERO,
							new KeyValue(contentContainer.translateYProperty(),
									(contentContainer.getLayoutY() + contentContainer.getLayoutBounds().getMaxY())
											* (topDirection ? -1 : 1),
									Interpolator.LINEAR),
							new KeyValue(overlay.opacityProperty(), 0, Interpolator.EASE_BOTH)),
					new KeyFrame(Duration.millis(1000),
							new KeyValue(overlay.opacityProperty(), 1, Interpolator.EASE_BOTH),
							new KeyValue(contentContainer.translateYProperty(), 0, Interpolator.EASE_OUT))));
			// reduce the number to increase the shifting , increase number to reduce
			// shifting
			setCycleDuration(Duration.seconds(0.4));
			setDelay(Duration.seconds(0));
		}
	}
}